当creator遇上protobufjs—叛逆成长
我们之前讲过要在Creator原生环境下使用protobufjs,使用伪装者的方式模拟nodejs的fs\path模块可以完美解决问题。但随着Creator1.7的到来,Shawn也尝了下鲜,但发现在creator模拟器环境下,原来的伪装方案失效了。
一、疑犯追踪
1. 调试神器
追踪Bug这个问题,不得不大赞一下Creator1.7提供的新的底层JS引擎,它使得在原生jsb环境上的调试手段、效率、体验都有了质的飞跃。在iOS/Mac平台使用Safari浏览器,Android/Windows可使用Chrome及Chrome的衍生调试工具。
上图是在Safari浏览器的调试界面,可以非常方便地在命令控制台上查看jsb上的对象、属性和方法,充分利用命令控制台的交互能力,它是学习js和cocos隐藏API的绝佳手段,特别是jsb函数。
2. 调试require函数
通过Safari的断点追踪,找到有一行protobufjs中的关键代码,require('fs')的返回值为undefined,请看下面代码
进入require函数调试,发现nameMap是一个以文件名为Key,文件路径为Value的一个对象,里面没找到fs,看下图。
通过这个nameMap我明白了为什么在Creator中可以直接require('文件名'),而不需要完整路径,同时也明白了为什么js文件不能同名的原因。
继续追踪问题,从下图的代码m.deps[request]中查看到fs与path的值都是等于2。
一步步的逼近问题的真相了,scripts数组的2号元素,是一个对象,指向的文件名为preview-scripts/__node_modules/browser-resolve/empty.js,并不是我们伪装的fs模块,请看下图:
从调试的结果来看,Creator模拟器将fs\path模块认为是nodejs的模块,没有按普通模块进行加载,随后向Creator引擎组最为热心的Jare请教此问题时得到证实。
二、一波三折
模拟的fs\path模块目前不能正常工作在Creator1.7模拟器,但在浏览器、自编译的MacApp、iOS、Android上都能正常运行。可是Creator模拟器是日常开发调试的利器,不能使用protobufjs库未免觉得遗憾。更要命的是,它会影响到我的pbkiller插件用户,面对这个问题绝对不可以马虎了事。
1. 明灯
发现问题的第一时间,我火速向引擎组的大大汇报了此问题,热心的Jare建议使用cc.loader.loadRes函数抹平不同平台上文件的加载问题。
当时眼前一亮,猛拍一下自己的脑袋,我以前怎么没想到这个办法?不论是Web\iOS\Android所有平台的文件加载都可以用cc.loader.loadRes搞定,比protobufjs中实现的fetch都简单多了,cc.loader.loadRes为我提供了一盏明灯。
2. 熄火
马上开始动手,但在准备动手前,我就想到绝对不能修改protobufjs的源码,因为我的pbkiller用户有些是用npm来管理的protobufjs,不可能让他们去修改node_moduls里的代码吧,这样太low了!
一束光在一片神经网络的触突上闪耀,电光石火的一瞬间,找到了一个方案
动态修改函数 + cc.loader.loadRes
请看下面代码,修改的Util.fetch方法
let protobuf = require('protobufjs');
protobuf.Util.fetch = function myfetch(path, callback) {
cc.loader.loadRes(path, (error, data) => {
if (!error) {
callback(data);
}
)}
};
正在得意之时,脑子里翁的一声,有问题?如果这样去实现protobufjs的fetch函数,只能是异步加载,而我之前给pbkiller的范例都是同步加载!眼前一黑,回过神来,绝对不能用这种方法坑了我的插件用户。
3. 曙光
不能修改protobufjs源码
保持同步与异步的加载接口
这两个方向如一座灯塔指引着我,我快速冷静下来,要一牯脑地胡打乱撞。在安静片刻过后,我开始重新对问题进行分析:
面临的问题是什么?
protobufjs库不能通过伪装的方式在creator1.7模拟器上工作,同时要考虑到pbkiller用户的同步加载习惯,不能单纯地使用cc.loader.loadRes的异步加载方案。
分析原因
应对办法
已经实验过在js语言中,为已经存在的函数赋值,可以在运行时修改函数的表现,它是实现继承、多态或勾子常见的做法,这是一个实用的技术。我可以要在运行时修改protobufjs中的关键函数,将其中的具体实现自己重写一次不就行了吗?
这样从物理表面上并没有修改源码,同时又可解决同步异步问题。
实施步骤
由于Creator的进化,经过调试分析,伪装者的策略存在了缺陷(就像人小的时候大人连蒙带骗,暂时把孩子给控制住了,但随着一孩子天天长大,他们的学习能力远超过大人的学习能力,原来的小把戏不适用了)。
重写下面两个函数:
Util.fetch
Builder.prototype[‘import’]
将其中调用nodejs模块代码摘掉,替换成Cocos jsb等价函数就可以解决问题。
三、逆境成长
经过上面对现状、问题、策略、步骤的自问自答,解决方法跃然纸上。看到这里有人可能会问,这不是四象限法法吗?
1. 四象限法
说实话最早我也不知道四象限法,它是这个周未我刚学到的新知识。当知道这种思考解决问题的方法时,我立刻就想起解决protobufjs在creator1.7模拟器上的问题,当时我不正是用的这种解决问题的吗?
打铁趁热,给大家介绍一下使用四象限法,把任何一个问题拆分成四个象限:
切开上下两部分,一个是现实,一个是理论;
切开左右两边,一边是过去,一边是未来;
从而构成思考问题的四个步骤,请看下图:
数据:问题是什么,描述过去的现实
分析:可能原因是什么,思考过去情况的理论原因
方向:应该采取的策略是什么,思考示未来情况的理论策略
下一步:具体的步骤是什么,思考未来情况的实现行动
这个思考过程有点像编写的一个数据转换函数的风格:
输入数据→解析数据→转换数据→生成结果
你还可以将生成的结果做为另一个函数的输入数据,构成一个可以循环使用的流程。
四象限法不仅是个思考工具,它还是一个行动实践指南,更多关于四象限法的知识可以参考《横向领导力》一书,它是我在得到App每天听本书栏目中无意见发现的,也推荐给你。
2. 引导
有了具体的实施步骤,不再废话了,直接上代码
1) 搞定Util.fetch
//导入protobufjs
let protobuf = require('protobufjs');
//保存原Util.fetch函数指针
let fetch = protobuf.Util.fetch;
//编写了一个myfetch函数,覆盖protobuf.Util.fetch变量
protobuf.Util.fetch = function myfetch(path, callbcak) {
//检查是否为原生环境
if (cc.sys.isNative) {
//原生环境直接使用jsb提供的文件操作函数加载proto内容
let str = jsb.fileUtils.getStringFromFile(path);
//如果是异步回调方式,使用callback参数返回数据
if (callbcak) {
callbcak(str);
return null;
}
//同步方式用返回值返回数据
return str;
}
//为web环境使用,protobufjs原来的处理函数
return fetch.call(this, path, callbcak);
};
通过上面的myfetch函数使用jsb.fileUtils.getStringFromFile轻松摘掉Util.fetch中的require(‘fs’)。
2) 拿下protobuf.Builder.prototype[‘import’]
有人可能会纳闷,为什么import函数要这样定义?
protobuf.Builder.prototype[‘import’] = function() { ... }
这是因为import是javascript中的关键字,不能定义一个名为import的函数,但可以为一个对象上定义一个import属性,在这里这个属性是一个函数。
//由于import函数代码太长,以下修改只给出了关键修改,主要是屏蔽代码。
protobuf.Builder.prototype['import'] = function(json, filename) {
var delim = '/';
// Make sure to skip duplicate imports
if (typeof filename === 'string') {
//--------------毙了-----------
// if (ProtoBuf.Util.IS_NODE)
// filename = require("path")['resolve'](filename);
//-----------------------------
if (this.files[filename] === true)
return this.reset();
this.files[filename] = true;
} else if (typeof filename === 'object') {
// Object with root, file.
var root = filename.root;
//--------------毙了-----------
// if (ProtoBuf.Util.IS_NODE)
// root = require("path")['resolve'](root);
//--------------------------------------------
if (root.indexOf("\\") >= 0 || filename.file.indexOf("\\") >= 0)
delim = '\\';
//--------------毙了-----------
//var fname;
// if (ProtoBuf.Util.IS_NODE)
// fname = require("path")['join'](root, filename.file);
// else
//----------------------------
var fname = root + delim + filename.file;
if (this.files[fname] === true)
return this.reset();
this.files[fname] = true;
} // Import imports
if (json['imports'] && json['imports'].length > 0) {
...
for (var i=0; i<json['imports'].length; i++) {
if (typeof json['imports'][i] === 'string') { // Import file
if (!importRoot)
throw Error("cannot determine import root");
var importFilename = json['imports'][i];
if (importFilename === "google/protobuf/descriptor.proto")
continue; // Not needed and therefore not used
//--------------毙了-----------
// if (ProtoBuf.Util.IS_NODE)
// importFilename = require("path")['join'](importRoot, importFilename);
// else
//-----------------------------
importFilename = importRoot + delim + importFilename;
if (this.files[importFilename] === true)
continue; // Already imported
...
} else // Import structure
...
}
if (resetRoot) // Reset import root override when all imports are done
this.importRoot = null;
}
// Import structures
if (json['package'])
this.define(json['package']);
if (json['syntax'])
propagateSyntax(json);
...
};
import函数又长又难看,耐着性子满以为把问题解决了,可运行起来时会发现新的错误:propagateSyntax函数没有定义。更气人的是它是protobufjs中的一个内部函数,没有放在任何对象之上,引不出来,没办法只能将propagateSyntax函数在当前上下文中再写一遍。
function propagateSyntax(parent) {
if (parent['messages']) {
parent['messages'].forEach(function(child) {
child["syntax"] = parent["syntax"];
propagateSyntax(child);
});
}
if (parent['enums']) {
parent['enums'].forEach(function(child) {
child["syntax"] = parent["syntax"];
});
}
}
还好,没再出现别的内部函数调用了,这下问题算是全部搞定了,终于我的程序可以运行起来了!
这段时间在学习如何带孩子,通过对protobufjs的几种解决方案对比看,我突然得出一些启示:
1. 修改源码好比是直接揍孩子,简单粗暴,但适应性差
2. 伪装是欺骗孩子,但随着孩子的成长,可能会失效
3. 动态修改函数,它是随时间或环境的变化,做出最正确的引导
耐心引导是最好的选择。
四、小结
简单小结一下,上面两个函数的修改操作还是有点小小差别
静态函数与原型函数
//修改静态函数 protobuf.Util.fetch = function myfetch(path, callbcak) {...} //修改原型函数 protobuf.Builder.prototype['import'] = function(json, filename) {...}
需要注意protobuf.Util.fetch是静态函数,而import是Builder原型函数,相当于是修改的成员函数。
缓存函数指针
//保存原Util.fetch函数指针 let fetch = protobuf.Util.fetch; //编写了一个myfetch函数,覆盖protobuf.Util.fetch变量 protobuf.Util.fetch = function myfetch(path, callbcak) { ... //调用原始操作 fetch.call(this, path, callback); }
有时候修改函数指针是为了做勾子监听或实现子类扩展,同时还要依赖原函数执行核心操作,这时就需要将原函数指针先保存起来。在适当的时机去调用,同时还要还原函数的this指针,所以要用函数的call方法,不能简单直接调用。
好了,以上就是今天的分享,希望能与Creator和大家一起叛逆成长。
欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起玩来玩吧!